# Localisation

> Supacharger uses [`next-intl`](https://next-intl.dev/docs/getting-started/app-router) with the Next.js App Router and **without locale-based routing**. Language is a user preference inside the SaaS application, so routes have one stable shape in every language:

# Localisation

Supacharger uses [`next-intl`](https://next-intl.dev/docs/getting-started/app-router) with the Next.js App Router and **without locale-based routing**. Language is a user preference inside the SaaS application, so routes have one stable shape in every language:

```text
/account       English, French, or another supported language
/en/account    Not used
/fr/account    Not used
```

Do not add a top-level `[locale]` route segment or `next-intl` routing middleware for the standard Supacharger setup.

## Marketing-site language handoff

A separate marketing application can pass its selected language into the authenticated SaaS by adding `lang` to the destination URL:

```text
https://app.example.com/login?lang=fr
https://app.example.com/login?plan=pro&lang=en
```

The standard query syntax is `?lang=fr`, not `lang?=fr`. Supacharger validates the value against `SUPPORTED_LOCALES`, writes an accepted value to the HTTP-only locale cookie, and redirects to the same pathname with only `lang` removed. Other query parameters, such as `plan=pro`, remain intact. The query parameter is therefore a one-time handoff and never becomes part of the application's route structure.

An unsupported, malformed, or uppercase value is ignored and removed. Add a language to `SUPPORTED_LOCALES` before linking to it from the marketing site.

## Application-owned files

The starter installs these localisation files:

```text
messages/
  en.json
  fr.json
src/i18n/
  config.ts
  request.ts
```

These files are developer-owned. `supacharger coreupdate` does not treat changes to them as core conflicts and does not add, overwrite, or delete catalogues when `messages/` already exists.

SnapScreen and Wakekeeper use these `src/` paths directly. Specdrive follows the same route and preference contract but uses repository-root `i18n/config.ts`, `i18n/request.ts`, and `messages/`. Every consumer supports the transient `?lang=<code>` handoff and profile restoration; translation coverage remains application-owned and does not imply that all existing UI copy is translated.

`src/i18n/config.ts` defines the supported locales, fallback locale, and cookie name:

```ts
export const SUPPORTED_LOCALES = ['en', 'fr'] as const;
export const DEFAULT_LOCALE = 'en';
export const LOCALE_COOKIE_NAME = 'supacharger_locale';
```

Keep a matching `messages/<locale>.json` file for every entry in `SUPPORTED_LOCALES`. The request configuration validates the cookie before loading a catalogue, so an unknown or malformed locale falls back to `DEFAULT_LOCALE` instead of attempting an arbitrary dynamic import.

## Request and client configuration

`src/i18n/request.ts` reads the locale preference and returns the locale and messages to `next-intl`. The root layout uses `getLocale()` and `getMessages()` and passes both to `NextIntlClientProvider`, which makes translations available to Server and Client Components.

Supacharger enables Next.js Cache Components in every application's merge-managed Next configuration. Since the validated locale cookie determines both `<html lang>` and the messages provider before the document can render, the developer-owned root layout exports `instant = false`. Next validates each route segment independently, so pages and nested layouts that directly perform request-time authentication also need an explicit boundary: `instant = false` for deliberately blocking identity/access checks, or local `Suspense` for streamable work. Cache Components makes request-time data dynamic by default; do not restore `dynamic = 'force-dynamic'` or cache user-specific authentication reads with `use cache`. The shared `test:auth-instant` contract checks these route segments.

The `/api/intl` endpoint changes the preference. Send a supported locale as JSON:

```ts
await fetch('/api/intl', {
  method: 'POST',
  headers: {'Content-Type': 'application/json'},
  body: JSON.stringify({locale: 'fr'})
});
```

The endpoint validates the locale and stores it in an HTTP-only, same-site cookie. For an authenticated user, it also stores the code in the profile. Reload or refresh the current route after a successful change. The pathname does not change.

## Profile persistence and precedence

The canonical profile column is `app.profiles.language_code`. It accepts lowercase two-letter codes (`^[a-z]{2}$`). The database constraint is deliberately language-agnostic; `SUPPORTED_LOCALES` is the application-level allowlist that controls which catalogues can actually be selected and loaded.

Supacharger resolves preferences in this order:

1. A valid `?lang=<code>` marketing handoff.
2. A valid `supacharger_locale` cookie created by an explicit selection.
3. The signed-in user's saved `language_code` when the browser has no valid locale cookie.
4. `DEFAULT_LOCALE`.

When a signed-out visitor arrives with a valid handoff, Supacharger stores the cookie and marks it for profile synchronisation. After the visitor signs in, the preference is written to the profile. On another browser or device with no locale cookie, the supported saved profile value is restored into the cookie.

Supacharger, SnapScreen, Wakekeeper, and Specdrive all persist this contract in `app.profiles.language_code`. Specdrive also exposes the language choice in Account Settings.

## Using translations

Use a namespace from the active application's catalogue:

```tsx
import {useTranslations} from 'next-intl';

export function SaveNotice() {
  const t = useTranslations('GlobalUI');
  return <p>{t('buttonSaved')}</p>;
}
```

Use `getTranslations` from `next-intl/server` in async Server Components, Server Actions, metadata, and route handlers.

## Authoring and translating copy

Always add or update the English source value in `messages/en.json` when a localised key or its wording changes. English is the authoring language and must be complete for the affected surface.

Before introducing or changing `useTranslations` or `getTranslations`, verify that its namespace and every statically referenced key exist with non-empty values in `messages/en.json`. Commit the consuming code and English catalogue together; otherwise `next-intl` raises `MISSING_MESSAGE` at runtime even when a secondary catalogue contains the namespace.

Do not invent, machine-translate, copy the English wording into, or otherwise fill French or another secondary-language value unless translation into that language is explicitly requested. If catalogue structure or a validation check requires the new key in a secondary catalogue, add the key with an empty string; otherwise leave that catalogue untouched. Empty strings are untranslated placeholders, not completed translations, and should be reported as pending translation.

Where a protected component supports older or incomplete developer-owned catalogues, keep an intentional English fallback so an empty or unavailable value does not expose a raw key or `MISSING_MESSAGE` error. Do not describe a locale or surface as translation-complete while placeholders remain.

## Adding a language

1. Add the locale to `SUPPORTED_LOCALES`.
2. Add `messages/<locale>.json` with every required namespace and key.
3. Add the locale to the application's language selector.
4. Test signed-out and signed-in screens, number/date formatting, and fallback behaviour.

Because catalogues are not CLI-managed, a core upgrade cannot insert newly required strings. Review the release notes, add the required English values, and add empty secondary placeholders only where catalogue structure requires them. Fill those placeholders only as part of explicitly requested translation work.
